<?php
/* Http服务
*
*
*/

namespace hpnWse {

/// Http服务
class stHttpSvc
{
	/// 配置项
	private static $e_AlwCors = false;
	private static $e_AppPubDiry = '';
	private static $e_AppSrcDiry = '';
	private static $e_AppName = '';
	private static $e_GlbUrlRoot = '';
	private static $e_AppUrlRoot = '';
	private static $e_PdoCfgPath = '';
	private static $e_RedisCfgPath = '';
	private static $e_LogCpct = 0;
	private static $e_RspsStas = null;

	// 处理计时
	private static $e_TimeBgn = null;

	// 数据库
	private static $e_Pdo = null;
	private static $e_Redis = null;

	/// 统计
	public static $c_SqlQryCnt = 0; // SQL查询数
	public static $c_CchHitCnt = 0; // 缓存命中数

	/// 正在运行？
	private static $e_Running = false;

	/// 环境，可以在这里放置“全局变量”
	public static $c_Ctxt = array();

	/// PDO配置
	///	{
	///	c_Dsn：String，数据源名，必须有效
	///	c_Usnm：String，用户名，必须有效
	///	c_Pswd：String，密码，必须有效
	/// c_Optns：Object，选项，默认null
	/// c_Ecdn: String，编码，默认"UTF8MB4"
	/// c_Salt：String，盐，可选
	///	}
	public static $c_PdoCfg = null;

	/// Redis配置
	/// {
	/// c_Host：String，主机，默认"127.0.0.1"
	/// c_Port: Number，端口，默认6379
	/// }
	public static $c_RedisCfg = null;

	/// 故障和故障回调
	/// $c_RspsFail：Object，
	/// {
	///	err_code：Number，错误码，0=成功，这里必须是非0值，＜0由WSE使用
	/// err_msg：String，错误消息，【只在开发模式下可用】
	/// err_src：String，错误源代码位置，【只在开发模式下可用】
	/// }
	/// $c_fOnFail：void f()，可以趁机修改$c_RspsFail的字段
	public static $c_RspsFail = null;
	public static $c_fOnFail = null;

	/// 调试数据，将作为JSON响应的属性"_debug"被返回，【注意，只在开发模式下可用】
	public static $c_Dbg = array();

	/// 元数据，属性将添加到返回的JSON里，和“data”并列
	public static $c_Meta = array();

	/// 初始化
	/// $a_Cfg：Object，配置对象，
	/// {
	/// c_NoHdlExcErr：Boolean，不要处理异常错误？默认false
	/// c_AlwCors：Boolean，允许CORS？默认false，开发模式下总是true
	/// c_AppPubDiry：String，应用程序发布目录，默认null表示入口文件所属目录的父目录
	/// c_AppSrcDiry：String，应用程序源代码目录，必须有效
	/// c_PdoCfgPath：String，PDO配置文件路径，如果要使用数据库则必须有效
	/// c_RedisCfgPath: 同上
	/// c_LogCpct：Number，日志容量（字节），当单份文件超出该值时，下次添加前会先删除前半部分，默认10MB
	/// c_RspsStas：Object，响应统计：
	/// 	{
	/// 	c_Time：Boolean，字段“Number use_time（毫秒）”，处理请求的用时（服务器执行时间，不含网络传输）
	/// 	c_Sql：Boolean，字段“Number sql_count（个数）”，执行SQL查询计数（来自stSqlUtil）
	///		c_Cch: Boolean，字段“Number cache_hit（个数）”，缓存命中数（来自tPgntCch）
	/// 	}
	/// }
	public static function cInit($a_Cfg = array())
	{
		// 检查配置项
		if (! isset($a_Cfg['c_AppSrcDiry']))
		{ throw new \Exception('“c_AppSrcDiry”必填！', -1); }

		// 记录
		self::$e_AlwCors = fIsDevMode() ? true : stObjUtil::cFchBoolPpty($a_Cfg, 'c_AlwCors');
		self::$e_AppPubDiry = stStrUtil::cEnsrDiry(realpath(stObjUtil::cFchPpty($a_Cfg, 'c_AppPubDiry', '../')));
		self::$e_AppSrcDiry = stStrUtil::cEnsrDiry(realpath($a_Cfg['c_AppSrcDiry']));
		$l_Len = stStrUtil::cGetLen(self::$e_AppSrcDiry);
		self::$e_AppName = stStrUtil::cSub(self::$e_AppSrcDiry, 
			stStrUtil::cRvsFind(self::$e_AppSrcDiry, '/', -2) + 1,
			$l_Len - 1);
		self::$e_PdoCfgPath = stObjUtil::cFchPpty($a_Cfg, 'c_PdoCfgPath', null);
		self::$e_RedisCfgPath = stObjUtil::cFchPpty($a_Cfg, 'c_RedisCfgPath', null);
		self::$e_LogCpct = stObjUtil::cFchPpty($a_Cfg, 'c_LogCpct', 1024 * 1024 * 10);

		// AppUrlRoot需要计算，因为开发时和发布时可能不同
		// 算法是，用发布目录去掉Web根（一定是前缀），再截取到应用程序名为止
		$l_UrlRoot = stStrUtil::cSub(self::$e_AppPubDiry, stStrUtil::cGetLen(fGetWebRootDiry()));
		$l_UrlRoot = stStrUtil::cSub($l_UrlRoot, 0, stStrUtil::cGetLen($l_UrlRoot) - stStrUtil::cGetLen(self::$e_AppName) - 1);
		self::$e_GlbUrlRoot = '/' . $l_UrlRoot;
		self::$e_AppUrlRoot = self::$e_GlbUrlRoot . self::$e_AppName . '/';

		$l_RspsStas = stObjUtil::cFchPpty($a_Cfg, 'c_RspsStas', array());
		self::$e_RspsStas = array(
			'c_Time' => stObjUtil::cFchPpty($l_RspsStas, 'c_Time', false),
			'c_Sql' => stObjUtil::cFchPpty($l_RspsStas, 'c_Sql', false),
			'c_Cch' => stObjUtil::cFchPpty($l_RspsStas, 'c_Cch', false)
		);

		//【配置异常和错误处理！】
		if (! fBool(stObjUtil::cFchPpty($a_Cfg, 'c_NoHdlExcErr', false)))
		{
		//	ini_set('display_errors', 0); // 不显示？
			error_reporting(E_ALL);
			set_exception_handler('\hpnWse\stHttpSvc::eHdlExc'); // 先
			set_error_handler('\hpnWse\stHttpSvc::eHdlErr'); // 后
			register_shutdown_function('\hpnWse\stHttpSvc::eHdlFtlErr');
		}
	}

	/// 日志
	public static function cLog($a_Info)
	{
		self::eLogLine($a_Info);
	}

	/// 获取应用程序发布目录
	public static function cGetAppPubDiry()
	{
		return self::$e_AppPubDiry;
	}

	/// 获取应用程序源目录
	public static function cGetAppSrcDiry()
	{
		return self::$e_AppSrcDiry;
	}

	/// 获取应用程序名称
	public static function cGetAppName()
	{
		return self::$e_AppName;
	}

	/// 获取全局（所有应用程序）URL根，类似于\hpnWse\fGetWebRoot，但更加通用
	public static function cGetGlbUrlRoot()
	{
		return self::$e_GlbUrlRoot;
	}

	/// 获取应用程序URL根，返回值可作为前缀，为当前应用程序生成URL
	public static function cGetAppUrlRoot()
	{
		return self::$e_AppUrlRoot;
	}

	/// 获取控制器目录
	public static function cGetCtrlDiry()
	{
		return self::$e_AppSrcDiry . 'nCtrl/';
	}

	/// 获取领域模型目录
	public static function cGetDmdlDiry()
	{
		return self::$e_AppSrcDiry . 'nDmdl/';
	}

	/// 是GET？
	public static function cByGet()
	{
		return 'GET' == fGetRqstMthd();
	}

	/// 是POST？
	public static function cByPost()
	{
		return 'POST' == fGetRqstMthd();
	}

	/// 是PUT？
	public static function cByPut()
	{
		return 'PUT' == fGetRqstMthd();
	}

	/// 是DELETE？
	public static function cByDelete()
	{
		return 'DELETE' == fGetRqstMthd();
	}

	/// 是PATCH？
	public static function cByPatch()
	{
		return 'PATCH' == fGetRqstMthd();
	}

	/// 获取查询参数
	public static function cFchQryPrm($a_Pn, $a_Dft = null)
	{
		return isset($_GET[$a_Pn]) ? $_GET[$a_Pn] : $a_Dft;
	}

	/// 获取体参数
	public static function cFchBodyPrm($a_Pn, $a_Dft = null)
	{
		return isset($_POST[$a_Pn]) ? $_POST[$a_Pn] : $a_Dft;
	}

	/// 运行
	/// a_RtnRst: Boolean，是否将结果返回给调用者？默认false直接发出响应
	/// 返回：控制器动作的结果
	public static function cRun($a_RtnRst = false)
	{
		// 运行
		self::$e_Running = true;

		if (self::$e_RspsStas['c_Time'])
		{ self::$e_TimeBgn = microtime(true); }

		// 提前设置CORS，以便客户端能够处理响应
		if (self::$e_AlwCors)
		{ self::cRspsHead_Cors(); }

		// 提取路由和模式，注意模式绝不能是1
		$l_C = self::cFchQryPrm('_c', 'index');
		$l_A = self::cFchQryPrm('_a', 'default');
		$l_M = ('1' === self::cFchQryPrm('_h', '0')) ? -1 : 0;
		
		// 执行控制器动作，参数来自$_REQUEST
		$l_Prms = self::cMakeCtrlActnPrms($_REQUEST, false); // 这里总是false，防止客户端篡改
		$l_RspsJson = self::cDoCtrlActn(fGetRqstMthd(), $l_C, $l_A, $l_Prms, $l_M);

		// 如果是array，响应JSON，其他类型忽略
		if (is_array($l_RspsJson))
		{
			// 如果需要，补上错误码0
			if (! array_key_exists('err_code', $l_RspsJson))
			{ $l_RspsJson['err_code'] = 0; }

			if (! $a_RtnRst)
			{ self::eRspsJson($l_RspsJson); }
		}
		return $l_RspsJson;
	}

	/// 制作控制器动作参数
	/// a_Cfg：
	/// {
	/// 【引擎参数】_FILES：默认 &$_FILES
	/// 【引擎参数】_SESSION：默认 &$_SESSION
	/// 【引擎参数】_NoVldt：该字段用于跳过验证，默认 false
	/// 应用程序参数……
	/// }
	/// a_NoVldt：Boolean，不要验证？默认null表示由a_Cfg决定，【警告：若a_Cfg来自客户端，务必传false！！】
	public static function cMakeCtrlActnPrms($a_Cfg, $a_NoVldt = false)
	{
		$l_Prms = array();
		if (isset($a_Cfg['_FILES'])) { $l_Prms['_FILES'] = &$a_Cfg['_FILES']; }
		else { $l_Prms['_FILES'] = &$_FILES; }
		if (isset($a_Cfg['_SESSION'])) { $l_Prms['_SESSION'] = &$a_Cfg['_SESSION']; }
		else { $l_Prms['_SESSION'] = &$_SESSION; }
		$l_Prms['_NoVldt'] = (null === $a_NoVldt) ? stObjUtil::cFchPpty($a_Cfg, '_NoVldt', false) : $a_NoVldt;

		foreach ($a_Cfg as $l_Pn => $l_Pv)
		{
			if ('_' == $l_Pn[0]) // 跳过引擎参数
			{ continue; }

			if (('json' === $l_Pn) && is_string($l_Pv)) // 如果是“json”，解码
			{ $l_Pv = stObjUtil::cDcdJson($l_Pv); }

			$l_Prms[$l_Pn] = $l_Pv;
		}
		return $l_Prms;
	}

	/// 执行控制器动作
	/// a_RM: String，请求方法，默认“GET”
	/// a_C：String，控制器，默认“index”
	/// a_A：String，动作，默认“default”
	/// a_Prms：Object，参数
	/// a_Mode: Number，模式，-1=帮助；0=依规则执行；1=无规则执行
	public static function cDoCtrlActn($a_RM, $a_C, $a_A, &$a_Prms, $a_Mode = 0)
	{
		// 计算控制器类文件路径
		$l_C = \hpnWse\fV1OrV2($a_C, 'index');
		$l_A = $a_RM; $l_A .= '_'; $l_A .= \hpnWse\fV1OrV2($a_A, 'default');
		$l_CPath = self::cGetCtrlDiry(); $l_CPath .= $l_C; $l_CPath .= '.php';

		// 若不存在就使用默认控制器的缺失动作
		if (!file_exists($l_CPath))
		{
			require_once(self::cGetCtrlDiry() . 'index.php');
			return call_user_func('\\hpnApp\\nCtrl\\stCtrl_index::_miss', $l_C, $l_A);
		}

		// 计算控制器类名
		$l_ClassName = '\\hpnApp\\nCtrl\\';
		if (stStrUtil::cFind($l_C, '/') >= 0) // 若含有nCtrl的子目录
		{
			$l_Parts = explode('/', $l_C);
			$l_PartLen = count($l_Parts);
			$l_C = $l_Parts[$l_PartLen - 1]; // 这是控制器的名字
			$l_Parts[$l_PartLen - 1] = ''; // 直接替换为空串，后面连结时就会以反斜杠结尾
		//	stAryUtil::cErs($l_Parts, $l_PartLen - 1);
			$l_ClassName .= implode('\\', $l_Parts);
		}
		$l_ClassName .= 'stCtrl_';
		$l_ClassName .= $l_C;

		// 按需加载类文件
		if (!class_exists($l_ClassName, false))
		{
			require_once($l_CPath);
			// $l_Fctn = $l_ClassName; $l_Fctn .= '::stCtor'; // 调用静态构造函数？【待定】
			// call_user_func($l_Fctn);
		}

		// 如果方法存在
		if (method_exists($l_ClassName, $l_A))
		{
			// 依规则执行？
			if (!$a_Mode)
			{
				$l_Ctrl = new $l_ClassName;
				$l_Rule = isset($l_Ctrl->c_Rules[$l_A]) ? $l_Ctrl->c_Rules[$l_A] : stObjUtil::cFchPpty($l_Ctrl->c_Rules, $a_RM);
				if (!$l_Rule) // null/false
				{ return self::cRspsErr(403, -2, '禁止访问'); }
				
				if (true === $l_Rule)
				{ $l_RspsErr = null; }
				else
				if (is_string($l_Rule))
				{
					$l_Agms = array();
					$l_Agms[] = &$a_Prms;
					$l_RspsErr = call_user_func_array($l_Rule, $l_Agms);
				}
				else
				{ $l_RspsErr = $l_Rule($a_Prms); }

				if ($l_RspsErr)
				{ return $l_RspsErr; }
			}

			// 调用
			$l_Fctn = $l_ClassName; $l_Fctn .= '::'; $l_Fctn .= $l_A;
			$l_Agms = array();
			$l_Agms[] = (-1 === $a_Mode);
			$l_Agms[] = &$a_Prms;
			return call_user_func_array($l_Fctn, $l_Agms);
		}
		else // miss
		{
			$l_Fctn = $l_ClassName; $l_Fctn .= '::_miss';
			return call_user_func($l_Fctn, $l_C, $l_A);
		}
	}

	/// 获取所有控制器动作帮助
	/// 返回：
	/// [
	/// 控制器名（无前缀） =>
	///		[
	///		动作名 => 
	///			[
	///			'F' => String 功能
	///			'P' => Array 参数
	///			'R' => Array 返回
	///			]
	///		]
	/// ]
	public static function cGetAllCtrlActnHelp()
	{
		$l_Rst = array();

		// 列出所有控制器和动作！
		// 先载入所有控制器，【TODO：暂时忽略模块！】
		$l_CtrlDiry = self::cGetCtrlDiry();
		self::eRqrAllCtrl($l_CtrlDiry);

		// 再找出所有控制器类，列出全部接口
		$l_AllClass = get_declared_classes();
		foreach($l_AllClass as $l_ClassName)
		{
			if (is_subclass_of($l_ClassName, '\\hpnWse\\stCtrl'))
			{
				// 开头都是“hpnApp\nCtrl\”
				$l_CtrlBgn = stStrUtil::cFind($l_ClassName, 'stCtrl_');
				$l_CtrlName = stStrUtil::cSub($l_ClassName, $l_CtrlBgn);

				$l_Fctn = $l_ClassName . '::sdGetAllActnHelp';
				$l_Rst[$l_CtrlName] = call_user_func($l_Fctn);
			//	$l_Agms = array(true);
			//	$l_EmtPrms = array();
			//	$l_Agms[] = &$l_EmtPrms;
			//	$l_Rst[$l_CtrlName] = call_user_func_array($l_Fctn, $l_Agms);
			}
		}

		return $l_Rst;
	}

	private static function eRqrAllCtrl($a_Diry)
	{
		$a_Diry = stStrUtil::cEnsrDiry($a_Diry);
		$l_Iter = new \DirectoryIterator($a_Diry);
		foreach ($l_Iter as $l_Flnm)
		{
			if (('.' == $l_Flnm) || ('..' == $l_Flnm))
			{ continue; }

			$l_Path = $a_Diry; $l_Path .= $l_Flnm;
			if (is_dir($l_Path))
			{ self::eRqrAllCtrl($l_Path); }
			else
			if (preg_match('/\.php$/i', $l_Flnm))
			{ require_once($l_Path); }
		}
	}

	/// 响应头 - 不要缓存
	public static function cRspsHead_NoCache()
	{
		header("Cache-Control: no-store, no-cache, must-revalidate");
		header('Pragma: no-cache'); // http 1.0
		header('Last-Modified: '. gmdate('D, d M Y H:i:s') . ' GMT');
		header('Expires: 0');
	}

	/// 响应头 - JSON
	public static function cRspsHead_Json()
	{
		header("Content-Type: application/json; charset=utf-8");
	}

	/// 响应头 - CORS
	public static function cRspsHead_Cors()
	{
		header('Access-Control-Allow-Origin: *');
	}

	/// 响应重定向
	/// $a_AbsUrl：String，绝对URL
	/// $a_Hsc：Number，状况码，301=永久，302（默认）=临时
	/// $a_NoExit：Boolean，不要终结当前脚本？
	public static function cRspsRdrct($a_AbsUrl, $a_Hsc = 302, $a_NoExit = false)
	{
		if (function_exists('http_response_code'))
		{
			http_response_code($a_Hsc);
		}
		else
		{
			header((301 == $a_Hsc) ? 'HTTP/1.1 301 Moved Permanently' : 'HTTP/1.1 302 Found');
		}
		header('Location: ' . $a_AbsUrl);
		if (! $a_NoExit)
		{ exit(0); }
	}

	/// 响应CGR（联合GET请求）
	/// $a_List：String$Object，键作为各自响应的名字，值是{ _m: 模块, _c: 控制器, _a: 动作, 其他参数…… }
	/// 	如果是String，则先用stObjUtil::cDcdJson解码再处理
	public static function cRspsCgr($a_List)
	{
		if (! \hpnWse\fBool($a_List))
		{
			$l_RspsJson = array('data' => null);
			return $l_RspsJson;
		}

		if (is_string($a_List))
		{ $a_List = stObjUtil::cDcdJson($a_List); }

		$l_RspsJson = array('data' => array());
		foreach ($a_List as $l_Key => $l_Item)
		{
			$l_C = stObjUtil::cFchPpty($l_Item, '_c');
			$l_A = stObjUtil::cFchPpty($l_Item, '_a');
			$l_Prms = self::cMakeCtrlActnPrms($l_Item, false); // 这里总是false，防止客户端篡改
			$l_RspsJson['data'][$l_Key] = self::cDoCtrlActn('GET', $l_C, $l_A, $l_Prms);
		}
		return $l_RspsJson;
	}

	/// 响应错误（一般用于请求错误4xx）
	/// a_Hsc：Number，状况码，常用的有 { 400=错误请求，401=未授权，403=禁止访问，500=服务器错误 }
	///		【警告：请认真设置本参数，原因见a_Data】
	/// a_Ec：Number，错误码，由程序自定义，0表示正确，所以应传非0值，
	/// 	一般的，-1=冬至引擎错误，-2=服务端程序错误，-3=客户端程序错误，-4=需刷新验证码
	/// a_Msg：String，信息，由程序自定义，存入响应的“err_msg”字段
	///		【警告：不要在这里提供任何敏感信息，粗略描述一下错误就好】
	/// a_Data：Object，数据，由程序自定义，存入响应的“data”字段
	/// 	【注意：发布模式下，若a_Hsc为5xx则忽略本参数，但会把全部四个参数写入日志】
	/// 返回：array
	public static function cRspsErr($a_Hsc, $a_Ec, $a_Msg, $a_Data = null)
	{
		$l_ErsData = (\hpnWse\fIsRlsMode() && (500 <= $a_Hsc) && ($a_Hsc <= 599));

		if (function_exists('http_response_code'))
		{
			http_response_code($a_Hsc);
		}
		else
		{
			header('HTTP/1.1 ' . $a_Hsc);
		}
		
		$l_RspsJson = array(
			'err_code' => (!$a_Ec) ? -2 : $a_Ec,
			'err_msg' => fV1OrV2($a_Msg, '未知错误'),
			'data' => $l_ErsData ? null : $a_Data
		);

		if ($l_ErsData) // 如果擦掉了，日志
		{
			if (is_array($a_Data))
			{ $a_Data = \hpnWse\stObjUtil::cEcdJson($a_Data); }

			self::eLogLine('【cRspsErr】 ' . $a_Hsc . ', ' . $a_Ec . ', “' . $a_Msg . '”, ' . $a_Data);
		}
		return $l_RspsJson;
	}

	/// 发出JSON
	/// a_Json: String$Array
	public static function cEchoJson(&$a_Json)
	{
		if (is_array($a_Json))
		{ $l_Json = stObjUtil::cEcdJson($a_Json); }
		else
		{ $l_Json = strval($a_Json); }
		
		if (ob_get_contents()) //【注意】先清除缓冲，因PHP解释器可能已经输出了一些HTML！
		{ ob_clean(); }
		
		self::cRspsHead_Json(); // 响应JSON
		echo $l_Json;
	}

	/// 设置会话过期时间，在session_start()之后调用
	/// a_Life：Number，生命（秒）
	public static function cSetSesnExpr($a_Life)
	{
		setcookie(session_name(), session_id(), (time() + $a_Life));
	}

	/// 连接到数据库
	/// a_Type: String∈{ "pdo", "redis" }
	public static function cCnctToDb($a_Type = 'pdo')
	{
		$a_Type = $a_Type ? strtolower($a_Type) : 'pdo';

		if ($a_Type == 'pdo')
		{
			if (self::$e_Pdo)
			{ return self::$e_Pdo; }

			if (! self::$c_PdoCfg)
			{ require(self::$e_PdoCfgPath); }

			self::$e_Pdo = new \PDO(
				self::$c_PdoCfg['c_Dsn'], 
				self::$c_PdoCfg['c_Usnm'],
				self::$c_PdoCfg['c_Pswd'],
				fV1OrV2(self::$c_PdoCfg['c_Optns'], null));
			self::$e_Pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); // 总是异常
			self::$e_Pdo->query('SET NAMES ' . self::$c_PdoCfg['c_Ecdn']); // 连接字符集
			return self::$e_Pdo;
		}
		else
		if ($a_Type == 'redis')
		{
			if (self::$e_Redis)
			{ return self::$e_Redis; }

			if (! self::$c_RedisCfg)
			{
				if (self::$e_RedisCfgPath)
				{
					require(self::$e_RedisCfgPath);
				}
				else
				{
					self::$c_RedisCfg['c_Host'] = array();
				}
			}

			self::$e_Redis = new \Redis();
			self::$e_Redis->connect(
				fV1OrV2(self::$c_RedisCfg['c_Host'], '127.0.0.1'),
				fV1OrV2(self::$c_RedisCfg['c_Port'], 6379)
			);
			return self::$e_Redis;
		}
		return null;
	}

	/// 存取PDO
	public static function cAcsPdo()
	{
		return self::$e_Pdo;
	}

	//------------------------【实现】

	private static function eHrc_500()
	{
		if (function_exists('http_response_code'))
		{
			http_response_code(500);
		}
		else
		{
			header('HTTP/1.1 500 Internal Server Error');
		}
	}

	// 拦截异常相关……
	public static function eHdlExc($a_Exc)
	{
	//	self::cLog('【异常123】'); //【未执行】
		self::eHrc_500();
		self::eCrtErrRspsBody($a_Exc->getCode(), $a_Exc->getMessage(), $a_Exc->getFile(), $a_Exc->getLine());
		self::eRspsFail();
	}

	public static function eHdlErr($a_ErrCode, $a_ErrMsg, $a_FilePath, $a_LineNum)
	{
	//	self::cLog('【异常456】'); //【最先，后面可能有其他】
		self::eHrc_500();
		self::eCrtErrRspsBody($a_ErrCode, $a_ErrMsg, $a_FilePath, $a_LineNum);
		self::eRspsFail();
	}

	public static function eHdlFtlErr()
	{
	//	self::cLog('【异常789】'); //【最后】
		$l_Err = error_get_last(); //最后一条错误信息
		if (! self::eIsFtlErr($l_Err)) // 只处理致命错误
		{ return; }

		self::eHrc_500();
		self::eCrtErrRspsBody($l_Err['type'], $l_Err['message'], $l_Err['file'], $l_Err['line']);
		self::eRspsFail();
	}

	private static function eIsFtlErr($a_Err)
	{
		static $i_FtlErrType = array(E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, 
								E_COMPILE_ERROR, E_COMPILE_WARNING);
		return in_array($a_Err['type'], $i_FtlErrType);
	}

	private static function eCrtErrRspsBody($a_ErrCode, $a_ErrMsg, $a_FilePath, $a_LineNum)
	{
		$l_EC = (0 === $a_ErrCode) ? -1 : $a_ErrCode; // 0表成功，非0表失败
		$l_EM = $a_ErrMsg;
		$l_ES = $a_FilePath . '(' . $a_LineNum . ')';

		self::$c_RspsFail = array('err_code' => $l_EC);
		if (fIsDevMode()) // 开发模式下，报告源文件和错误消息（可能含有敏感信息）
		{
			self::$c_RspsFail['err_msg'] = $l_EM;
			self::$c_RspsFail['err_src'] = $l_ES;
		}

		self::eLogLine('【异常】' . $l_EC . '：' . $l_EM . ' ← “' . $l_ES . '”。');
	}

	private static function eLogLine($a_Info)
	{
		$l_LogDiry = self::$e_AppSrcDiry . 'Logs';
		if (!file_exists($l_LogDiry))
		{
			mkdir($l_LogDiry);
		}

		$l_LogDiry .= '/';
		$l_LogName = date('Y-m-d', time());
		$l_LogPath = $l_LogDiry . $l_LogName . '.txt';
		$l_NowDatm = stDateUtil::cNowDatm();
		$l_Info = '【' . $l_NowDatm . '】' . $a_Info . PHP_EOL;
		
		// // 检查容量限制
		// $l_FileSize = file_exists($l_LogPath) ? filesize($l_LogPath) : 0;
		// if ($l_FileSize > self::$e_LogCpct)
		// {
		// 	// 删除约一半的内容（基于行），
		// 	// 首先读出要删的行，
		// 	// 然后把剩余行转移到一个临时文件里，
		// 	// 最后删除现有文件，重命名临时文件。
		// 	$l_FH = fopen($l_LogPath, "r");
		// 	flock($l_FH, LOCK_EX); // 独占锁
		// 	$l_Acc = 0;
		// 	$l_ErsSize = self::$e_LogCpct >> 1;
		// 	while (($l_Acc < $l_ErsSize)) // && (! feof($l_FH))
		// 	{
		// 		$l_Line = fgets($l_FH);
		// 		$l_Acc += stStrUtil::cGetLen($l_Line);
		// 	}

		// 	$l_TempPath = $l_LogDiry . $l_LogName . '_T' . stStrUtil::cRandNum(8) . '.txt';
		// 	$l_TH = fopen($l_TempPath, "w");
		// 	fputs($l_TH, ('【———————————— ' . $l_NowDatm . ' 超出容量，截断！————————————】' . PHP_EOL));
		// 	while ((! feof($l_FH)))
		// 	{
		// 		$l_Line = fgets($l_FH); // 返回值里含有换行符
		// 		fputs($l_TH, $l_Line);
		// 	}
		// 	fputs($l_TH, $l_Info);
		// 	flock($l_FH, LOCK_UN); //【？在这里解锁，会不会有问题？！】
		// 	fclose($l_FH);
		// 	fclose($l_TH);
		// 	unlink($l_LogPath);
		// 	rename($l_TempPath, $l_LogPath);
		// }
		// else
	//	{
			file_put_contents($l_LogPath, $l_Info, (FILE_APPEND | LOCK_EX)); // 独占锁
	//	}
	}

	private static function eRspsFail()
	{
		// // 日志异常SQL
		// if (class_exists('\hpnWse\stSqlUtil') && \hpnWse\stSqlUtil::$uc_ExcSql)
		// {
		// 	self::cLog('【异常SQL】' . \hpnWse\stSqlUtil::$uc_ExcSql);
		// }

		// 防止回调期间再次异常，响应完毕立即终结
	//	try
	//	{
			if (self::$c_fOnFail)
			{ self::$c_fOnFail(); }
	//	}
	//	finally //【旧版不支持】
	//	{
			// 如果通过cRun引发的则响应JSON，否则忽略
			if (self::$e_Running)
			{
				self::eRspsJson(self::$c_RspsFail);
				exit(1);
			}
	//	}
	}

	private static function eRspsJson(&$a_RspsBody)
	{
		// 附加调试数据和元数据，【注意：空数组转为false】
		if (\hpnWse\fIsDevMode() && self::$c_Dbg)
		{ $a_RspsBody['_debug'] = self::$c_Dbg; }

		foreach (self::$c_Meta as $l_K => $l_V)
		{ $a_RspsBody[$l_K] = $l_V; }

		// 响应统计
		if (self::$e_RspsStas['c_Sql'] && class_exists('\hpnWse\stSqlUtil'))
		{ $a_RspsBody['sql_count'] = self::$c_SqlQryCnt; }

		if (self::$e_RspsStas['c_Cch'] && class_exists('\hpnWse\tPgntCch'))
		{ $a_RspsBody['cache_hit'] = self::$c_CchHitCnt; }

		if (self::$e_RspsStas['c_Time']) // 最后统计用时（毫秒），保留到小数点后三位（微秒）
		{ $a_RspsBody['use_time'] = round((microtime(true) - self::$e_TimeBgn) * 1e6) / 1000; }
		
		// 发出JSON
		self::cEchoJson($a_RspsBody);
	}
} // class stHttpSvc


/// 服务处理单元（Service Processing Unit），作为基类使用
class stSpu
{
	/// 执行
	/// $a_Prms：Object，参数
	/// 返回：任意
	public static function cRun($a_Prms)
	{
		return array();
	}
}

/// 控制器，作为基类使用
class stCtrl
{
	/// 接口的全局规则，形如：
	/// array(
	/// 'GET_read' => function (&$a_Prms) { return [已登录](); },
	/// 'POST_update' => function (&$a_Prms) { return [管理员](); },
	/// )
	public static $c_GlbRules = array(
		/// 默认GET规则：允许访问
		/// 默认POST、PUT、DELETE规则：禁止访问
		'GET' => true,
		'POST' => false,
		'PUT' => false,
		'DELETE' => false,
	);

	/// 接口的类规则，【注意：构造函数内只能填写规则】
	public $c_Rules;
	public function __construct()
	{
		$this->c_Rules = self::$c_GlbRules;
	}

	/// 缺失
	public static function _miss($a_Ctrl, $a_Actn)
	{
		return stHttpSvc::cRspsErr(404, -1, 
			('控制器动作“' . $a_Ctrl . '::' . $a_Actn . '”不存在！'));
	}

	/// 默认
	/// $a_Help：Boolean，帮助？
	/// $a_Prms：Object，参数
	public static function GET_default($a_Help, &$a_Prms)
	{
		// 返回所有动作帮助文档
		return self::sdGetAllActnHelp();
	}

	//【这个应该放到派生类里，放在基类导致所有派生类都会继承】
	// public static function GET_cgr($a_Help, &$a_Prms)
	// {
	// 	if ($a_Help)
	// 	{
	// 		return self::sdHelp('响应联合GET请求', 
	// 			array('json' => '列出要调用的接口参数，为每个请求起个名字作为键'), 
	// 			array('data' => '对应各个GET请求的响应，键就是请求参数“json”里的键'));
	// 	}

	// 	return stHttpSvc::cRspsCgr(stObjUtil::cFchPpty($a_Prms, 'json'));
	// }

	/// 帮助
	public static function sdHelp($a_Fctn, $a_Prms = null, $a_Rsps = null, $a_Err = null)
	{
		$l_Rst = array(
			'F' => $a_Fctn,
			'P' => $a_Prms ? $a_Prms : 'void',
			'R' => $a_Rsps ? $a_Rsps : 'void',
			'E' => $a_Err ? $a_Err : 'void'
		);
		return $l_Rst;
	}

	/// 获取所有动作帮助文档
	public static function sdGetAllActnHelp()
	{
		// 确定派生类的类型
		$l_DrvdIstn = new static();
		$l_ClassName = get_class($l_DrvdIstn);
	//	$l_CtrlBgn = stStrUtil::cFind($l_ClassName, 'stCtrl_');
	//	$l_CtrlName = stStrUtil::cSub($l_ClassName, $l_CtrlBgn);

		$l_Rst = array();
		$l_AllMthd = get_class_methods($l_ClassName);
		foreach($l_AllMthd as $l_MthdName)
		{
			if ('GET_default' == $l_MthdName) // 注意不要无限间接递归！
			{ continue; }

			if (preg_match('/^(?:GET|POST)_.+$/i', $l_MthdName))
			{
				$l_Fctn = $l_ClassName . '::' . $l_MthdName;
				$l_Agms = array(true);
				$l_EmtPrms = array();
				$l_Agms[] = &$l_EmtPrms;
				$l_Rst[$l_MthdName] = call_user_func_array($l_Fctn, $l_Agms);
			}
		}
		return $l_Rst;
	}

	// /// HTTP服务响应CUD异常，主要用于在验证期间捕获的异常
	// /// $a_Msg：String，消息，默认“无法完成请求，提交的数据含有错误！”【警告】不要传入敏感消息！异常的消息里可能含有秘密！
	// /// $a_ErrInfo：JSON，错误信息，详见stHttpSvc::cRspsErr
	// /// $a_Exc：Exception，捕获的异常，只在开发模式下返回，作为$a_ErrInfo的“_PhpException”属性
	// public static function sdHttpSvcRspsCudExc($a_Msg = null, $a_ErrInfo = null, $a_Exc = null)
	// {
	// 	if (! \hpnWse\fBool($a_Msg)) { $a_Msg = '无法完成请求，提交的数据含有错误！'; }
	// 	if (! \hpnWse\fBool($a_ErrInfo)) { $a_ErrInfo = array(); }
	// 	if (\hpnWse\fIsDevMode() && $a_Exc)
	// 	{
	// 		$a_ErrInfo['_PhpException'] = array(
	// 			'Code' => $a_Exc->getCode(),
	// 			'Msg' => $a_Exc->getMessage(), 
	// 			'File' => $a_Exc->getFile(), 
	// 			'Line' => $a_Exc->getLine()
	// 		);
	// 	}
	// 	return \hpnWse\stHttpSvc::cRspsErr(400, -3, $a_Msg, $a_ErrInfo);
	// }
}

} // namespace hpnWse


//////////////////////////////////// OVER ////////////////////////////////////